﻿using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Research.Kinect.Nui;
using System.Linq;
using System.Windows.Threading;
using Emgu.CV;
using System.Collections.Generic;
using Emgu.CV.Structure;
using Emgu.CV.CvEnum;
using SoGame.Model;
using System.Linq;
using System.Drawing;
using System.Windows;
using Coding4Fun.Kinect.Wpf;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace SoGame.Helper
{
    /// <summary>
    /// 
    /// </summary>
    public class KinectHelper : INotifyPropertyChanged
    {
        #region Private Fields

        private const int RUNTIME_ALREADY_USED_ERROR_CODE = -2147220947;
        private object _sync = new object();
        private static KinectHelper _Instance;

        private Runtime _Kinect;
        private BitmapSource _ImageSource;
        private string _ImageName;
        private RuntimeOptions _RuntimeOptions;
        private string _StatusMessage;

        //Declararation of all variables, vectors and haarcascades
        private Capture capture;
        private HaarCascade haarCascade;
        DispatcherTimer timer;
        int ContTrain = 0;
        int NumLabels, t;
        Image<Gray, byte> result, TrainedFace = null;
        Image<Gray, byte> gray = null;
        Image<Bgr, Byte> currentFrame;
        List<Image<Gray, byte>> trainingImages = new List<Image<Gray, byte>>();
        List<string> labels = new List<string>();
        List<string> NamePersons = new List<string>();
        string name, names = null;
        MCvFont font = new MCvFont(FONT.CV_FONT_HERSHEY_TRIPLEX, 0.8d, 0.8d);

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets ImageSource
        /// </summary>
        public BitmapSource ImageSource
        {
            get { return _ImageSource; }
            set
            {
                _ImageSource = value;
                RaisePropertyChanged("ImageSource");
            }
        }

        public string ImageName
        {
            get { return _ImageName; }
            set
            {
                _ImageName = value;
                RaisePropertyChanged("ImageName");
            }
        }

        private Player _currentPlayer;
        public Player CurrentPlayer
        {
            get { return _currentPlayer; }
            set
            {
                _currentPlayer = value;
                RaisePropertyChanged("CurrentPlayer");
            }
        }

        private ObservableCollection<Player> _players = new ObservableCollection<Player>();
        public ObservableCollection<Player> Players
        {
            get { return _players; }
            set
            {
                _players = value;
                RaisePropertyChanged("Players");
            }
        }

        /// <summary>
        /// Gets or sets Kinect (Runtime)
        /// </summary>
        public Runtime Kinect
        {
            get { return _Kinect; }
            set
            {
                if (_Kinect != null)
                {
                    UninitializeKinectServices(_Kinect);
                }
                _Kinect = value;
                if (_Kinect != null)
                {
                    InitializeKinectServices(_Kinect);
                }
            }
        }

        /// <summary>
        /// Gets or sets Status Message
        /// </summary>
        public string StatusMessage
        {
            get { return _StatusMessage; }
            set
            {
                _StatusMessage = value;
                RaisePropertyChanged("StatusMessage");
            }
        }

        #endregion

        public KinectStatus Statut { get { return Kinect.Status; } }

        public static KinectHelper Instance
        {
            get { return _Instance ?? (_Instance = new KinectHelper()); }
        }

        public KinectHelper() { }

        #region Private Methods

        private void KinectDiscovery()
        {
            Runtime.Kinects.StatusChanged += new EventHandler<StatusChangedEventArgs>(Kinects_StatusChanged);

            //foreach (Runtime kinect in Runtime.Kinects)
            //{
            //    if (kinect.Status == KinectStatus.Connected)
            //    {
            //        if (Kinect == null)
            //        {
            //            Kinect = kinect;
            //            return;
            //        }
            //    }
            //}

            Kinect = Kinect ?? Runtime.Kinects.Where(k => k.Status == KinectStatus.Connected).FirstOrDefault();

        }

        private SoGameDbContext context = new SoGameDbContext();

        private void InitializeKinectServices(Runtime runtime)
        {
            _RuntimeOptions = RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor ;

            try
            {
                Kinect.Initialize(_RuntimeOptions);
                Kinect.SkeletonEngine.TransformSmooth = true;
                Kinect.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(OnSkeletonFrameReady);
                Kinect.DepthFrameReady += new EventHandler<ImageFrameReadyEventArgs>(OnDepthFrameReady);
                Kinect.VideoFrameReady += new EventHandler<ImageFrameReadyEventArgs>(OnVideoFrameReady);
                Kinect.NuiCamera.ElevationAngle = 10;
                runtime.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color);
                runtime.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex);
            }
            catch (COMException comException)
            {
                if (comException.ErrorCode == RUNTIME_ALREADY_USED_ERROR_CODE)
                {
                    Kinect = null;
                    _StatusMessage = "KinectAppConflict";
                    return;
                }
                else
                {
                    throw comException;
                }
            }
        }

        private void UninitializeKinectServices(Runtime runtime)
        {
            Kinect.Uninitialize();
            Runtime.Kinects.StatusChanged -= new EventHandler<StatusChangedEventArgs>(Kinects_StatusChanged);
            Kinect.SkeletonFrameReady -= new EventHandler<SkeletonFrameReadyEventArgs>(OnSkeletonFrameReady);
            Kinect.DepthFrameReady -= new EventHandler<ImageFrameReadyEventArgs>(OnDepthFrameReady);
            Kinect.VideoFrameReady -= new EventHandler<ImageFrameReadyEventArgs>(OnVideoFrameReady);
        }

        #region Private Events

        Bitmap currentImage;

        private void OnVideoFrameReady(object sender, ImageFrameReadyEventArgs e)
        {
            //Stopwatch wathgeneral = new Stopwatch();
            //wathgeneral.Start();
            
            if (IsRecognitionStarted)
            {
                // Get the current frame form capture device
                //OLD: currentImage = (Bitmap)tc.ConvertFrom(ImageHelper.ConvertImagePNGToByteArray(e.ImageFrame.ToBitmapSource()));
                BitmapSource bSource = BitmapSource.Create(e.ImageFrame.Image.Width, e.ImageFrame.Image.Height, 96, 96, PixelFormats.Bgr32, null, e.ImageFrame.Image.Bits, e.ImageFrame.Image.Width * e.ImageFrame.Image.BytesPerPixel);
                currentImage = ImageHelper.GetBitmapFromBitmapSource(bSource);

                //Get the current frame form capture device
                currentFrame = new Image<Bgr, Byte>(currentImage);
                
                if (currentFrame != null)
                {
                    //Convert it to Grayscale
                    Image<Gray, Byte> grayFrame = currentFrame.Convert<Gray, Byte>();

                    CurrentPlayer = null;
                    Players.Clear();

                    //Face Detector
                    MCvAvgComp[][] facesDetected = grayFrame.DetectHaarCascade(
                      haarCascade,
                      1.2,
                      2,
                      Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
                      new System.Drawing.Size(80, 80));

                    List<Image<Gray, byte>> listtemptrainingImages = new List<Image<Gray,byte>>(trainingImages);
                    List<string> listlabels = new List<string>(labels);
                    
                    //Action for each element detected
                    foreach (MCvAvgComp f in facesDetected[0])
                    {
                        result = grayFrame.Copy(f.rect).Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
                        //draw the face detected in the 0th (gray) channel with blue color
                        currentFrame.Draw(f.rect, new Bgr(38, 191, 140), 3);

                        if (listtemptrainingImages.ToArray().Length != 0)
                        {
                            //TermCriteria for face recognition with numbers of trained images like maxIteration
                            MCvTermCriteria termCrit = new MCvTermCriteria(listtemptrainingImages.Count, 0.001);

                            //Eigen face recognizer
                            EigenObjectRecognizer recognizer = new EigenObjectRecognizer(
                               listtemptrainingImages.ToArray(),
                               listlabels.ToArray(),
                               5000,
                               ref termCrit);
                            //Debug.WriteLine("Train images : {0}, labels size {1}", trainingImages.Count, labels.Count);

                            string[] keys = recognizer.Recognize(result).Split('%');
                            if (keys.Length > 1)
                            {
                                name = keys[1];
                                int key = Convert.ToInt32(keys[0]);

                                //Draw the label for each face detected and recognized
                                CurrentPlayer = context.GetPlayer(key);
                                //Debug.WriteLine("Player found : {0} {1} {2} {3}", keys[0], keys[1], CurrentPlayer.FirstName, CurrentPlayer.Name);
                                currentFrame.Draw(string.Format("{0} {1}", CurrentPlayer.FirstName, CurrentPlayer.Name), ref font, new System.Drawing.Point(f.rect.X - 2, f.rect.Y - 2), new Bgr(38, 191, 140));

                                string label = listlabels.Where(it => it.Split('%')[0] == CurrentPlayer.ID.ToString()).FirstOrDefault();
                                if (label != null)
                                {
                                    int index = listlabels.IndexOf(label);
                                    listtemptrainingImages.RemoveAt(index);
                                    listlabels.Remove(label);
                                }
                            }

                            if (!Players.Contains(CurrentPlayer))
                                Players.Add(CurrentPlayer);
                        }
                    }

                    Debug.WriteLine("Faces detected : {0}, Players detected {1}", facesDetected[0].Length, Players.Count);

                    ////Names concatenation of persons recognized
                    //for (int nnn = 0; nnn < facesDetected[0].Length; nnn++)
                    //{
                    //    names = names + NamePersons[nnn] + ", ";
                    //}
                    ////Show the faces procesed and recognized
                    ////imageBox1.Source = ToBitmapSource(currentFrame);
                    //txtPersons = names;
                    //names = "";
                    ////Clear the list(vector) of names
                    //NamePersons.Clear();

                    ImageSource = ImageHelper.ToBitmapSource(currentFrame);
                }
            }
            else
            {
                ImageSource = e.ImageFrame.ToBitmapSource();
            }

            //wathgeneral.Stop();
            //Debug.WriteLine(string.Format("Total : {0}", wathgeneral.ElapsedMilliseconds.ToString()));
        }

        private void OnDepthFrameReady(object sender, ImageFrameReadyEventArgs e) 
        { }

        private void OnSkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        { }

        private void Kinects_StatusChanged(object sender, StatusChangedEventArgs e)
        {
            switch (e.Status)
            {
                case KinectStatus.Connected:
                    if (Kinect == null)
                    {
                        Kinect = e.KinectRuntime; //if Runtime.Init() fails due to an AppDeviceConflict, this property will be null after return.
                        _StatusMessage = "None";
                    }
                    break;
                case KinectStatus.Disconnected:
                    if (Kinect == e.KinectRuntime)
                    {
                        Kinect = null;
                    }
                    break;
                case KinectStatus.NotReady:
                    if (Kinect == null)
                    {
                        _StatusMessage = "NotReady";
                    }
                    break;
                case KinectStatus.NotPowered:
                    if (Kinect == e.KinectRuntime)
                    {
                        Kinect = null;
                        _StatusMessage = "NoPower";
                    }
                    break;
                default:
                    throw new Exception("Unhandled Status: " + e.Status);
            }
            if (Kinect == null)
            {
                _StatusMessage = "NoKinect";
            }
        }

        #endregion

        #endregion

        #region Public Methods

        /// <summary>
        /// Starts Kinect Device
        /// </summary>
        public void StartKinect()
        {
            KinectDiscovery();

            if (Kinect == null)
            {
                if (Runtime.Kinects.Count == 0)
                {
                    _StatusMessage = "NoKinect";
                }
                else
                {
                    if (Runtime.Kinects[0].Status == KinectStatus.NotPowered)
                    {
                        _StatusMessage = "NoPower";
                    }
                }
            }
        }

        /// <summary>
        /// Pause Kinect Device
        /// </summary>
        public void PauseKinect()
        {
            if (Kinect != null)
            {
                StopRecognition();
            }
        }

        /// <summary>
        /// Stops Kinect Device
        /// </summary>
        public void StopKinect()
        {
            if (Kinect != null)
            {
                StopRecognition();
                Kinect = null;
            }
        }

        /// <summary>
        /// Starts the FAce Recognition
        /// </summary>
        public void StartRecognition()
        {
            //Recognition start here
            haarCascade = new HaarCascade(@"haarcascade_frontalface_alt_tree.xml");

            //Load of previus trainned faces and labels for each image
            IQueryable<Player> players = context.GetAllPlayers();
            trainingImages.Clear();
            labels.Clear();
            ContTrain = 1;
            foreach (var item in players)
            {
                if (item.Photo != null)
                {
                    //ContTrain += 1;
                    TypeConverter tc = TypeDescriptor.GetConverter(typeof(Bitmap));
                    Bitmap image = (Bitmap)tc.ConvertFrom(item.Photo);
                    trainingImages.Add(new Image<Gray, byte>(image));

                    labels.Add(string.Format("{0}%{1}", item.ID, item.Name));
                }
            }
        }

        public void StopRecognition()
        {
            haarCascade = null;
            CurrentPlayer = null;
            Players.Clear();
        }

        /// <summary>
        /// Gets if Kinect Runtime is started
        /// </summary>
        public bool IsKinectStarted
        {
            get { return Kinect != null; }
        }

        /// <summary>
        /// Gets if facial recognition is actived
        /// </summary>
        public bool IsRecognitionStarted
        {
            get { return haarCascade != null; }
        }

        /// <summary>
        /// Takes a picture
        /// <param name="filePath">file path to saved picture</param>
        /// </summary>
        public void TakePicture(out string filePath)
        {
            filePath = string.Empty;
            if (IsKinectStarted)
            {
                filePath = Path.GetRandomFileName() + ".png";
                using (var fileStream = File.Create(ImageName))
                {
                    var encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(ImageSource));
                    encoder.Save(fileStream);
                }
            }
        }

        /// <summary>
        /// Takes a picture
        /// </summary>
        /// <returns></returns>
        public Stream TakePicture()
        {
            if (IsKinectStarted)
            {
                MemoryStream ms = new MemoryStream();
                var encoder = new PngBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(ImageSource));
                encoder.Save(ms);
                return ms;
            }
            return null;
        }

        public BitmapSource TakePictureFace()
        {
            if (IsKinectStarted)
            {
                try
                {
                    //Trained face counter
                    ContTrain = ContTrain + 1;

                    //Get a gray frame from capture device
                    Image<Gray, byte> tst = new Image<Gray, byte>(currentImage);
                    gray = tst.Resize(320, 240, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);

                    //Face Detector
                    MCvAvgComp[][] facesDetected = gray.DetectHaarCascade(
                    haarCascade,
                    1.2,
                    10,
                    Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
                    new System.Drawing.Size(20, 20));

                    //Action for each element detected
                    foreach (MCvAvgComp f in facesDetected[0])
                    {
                        TrainedFace = currentFrame.Copy(f.rect).Convert<Gray, byte>();
                        break;
                    }

                    //resize face detected image for force to compare the same size with the 
                    //test image with cubic interpolation type method
                    TrainedFace = result.Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
                    trainingImages.Add(TrainedFace);

                    if (CurrentPlayer != null)
                        labels.Add(CurrentPlayer.Name);
                    else
                        labels.Add("test");

                    //Show face added in gray scale
                    ImageSource = ImageHelper.ToBitmapSource(TrainedFace);
                    //imageBox1.Source = ToBitmapSource(TrainedFace);

                    ////Write the number of triained faces in a file text for further load
                    //File.WriteAllText(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/TrainedLabels.txt", trainingImages.ToArray().Length.ToString() + "%");

                    ////Write the labels of triained faces in a file text for further load
                    //for (int i = 1; i < trainingImages.ToArray().Length + 1; i++)
                    //{
                    //    trainingImages.ToArray()[i - 1].Save(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/face" + i + ".bmp");
                    //    File.AppendAllText(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/TrainedLabels.txt", labels.ToArray()[i - 1] + "%");
                    //}
                    return ImageHelper.ToBitmapSource(TrainedFace);
                    //MessageBox.Show(textBox1.Text + "´s face detected and added :)", "Training OK", MessageBoxButton.OK);// MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch
                {
                    //MessageBox.Show("Enable the face detection first", "Training Fail", MessageBoxButton.OK);// MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                }
            }
            return null;
        }

        public void KinectCameraUp()
        {
            //Max 22
            if (Kinect.NuiCamera.ElevationAngle <= 16)
            {
                Kinect.NuiCamera.ElevationAngle += 6;
            }
        }

        public void KinectCameraDown()
        {
            //Min 26
            if (Kinect.NuiCamera.ElevationAngle >= -20)
            {
                Kinect.NuiCamera.ElevationAngle -= 6;
            }
        }

        #endregion

        #region INotifyPropertyChanged Implementation

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}
